上篇介绍了React的基本使用,这篇博客介绍一下著名的React Hooks
<!--more-->
Hooks简介
注意:React 16.8.0 是第一个支持 Hook 的版本
Hooks
是一些可以让你在函数组件里“钩入”React state
及生命周期等特性的函数。其提供了使函数式组件可以使用和Class
组件一样的特性的方法,例如useState
可以让函数式组件也拥有state
等。
Hooks
带来的好处有:
- 可以使用
Hook
从组件中提取状态逻辑,使得这些逻辑可以单独测试并复用。Hook
使你在无需修改组件结构的情况下复用状态逻辑 Hook
将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),而并非强制按照生命周期划分。Hook
使你在非class
的情况下可以使用更多的React
特性
Hooks的使用原则
Hook
就是JavaScript
函数,但是使用它们会有两个额外的规则:
- 只能在函数最外层调用
Hook
。不要在循环、条件判断或者子函数中调用。 - 只能在
React
的函数组件中调用Hook
。不要在其他JavaScript
函数中调用。(当然自定义的Hook
中也可以调用)
State Hook
State Hook的简单使用
先来看一个最简单的使用useState()
的例子:
import React, {useState} from "react";
export default () => {
const [counter, setCounter] = useState(0); // 0代表给counter的初始值为0
let btnClick = () => (
setCounter(counter + 1)
);
return (
<div>
<h1>LearnHooks</h1>
<div>current Counter: {counter}</div>
<button onClick={btnClick}>click to plus counter</button>
</div>
)
}
上述组件如果使用Class
组件的写法,等价于:
export default class LearnReact extends React.Component {
constructor(props) {
super(props);
this.state = {
counter: 0,
}
}
btnClick = () => {
this.setState((prevState) => {
return {
counter: prevState.counter + 1
}
})
};
render() {
return (
<div>
<h1>LearnHooks</h1>
<div>current Counter: {this.state.counter}</div>
<button onClick={this.btnClick}>click to plus counter</button>
</div>
)
}
}
仔细体会一下两种写法的差异和优劣性,下面来仔细分析一下state hook
的使用:
const [counter, setCounter] = useState(0)
这句话定义了一个
counter
变量和一个用来修改定义的counter
变量的方法setCounter()
,定义的这个变量它与class
组件里面的this.state
提供的功能完全相同。一般来说,在函数退出后变量就就会”消失”,而state
中的变量会被React
保留。useState(0)
useState()
只接收一个参数即定义的state
变量的初始值,可以是对象也可以是其他原始类型等等。而Class
组件的初始值则一定是在this.state
中的这个对象里的属性,这是一个区别点。useState()
的返回值经过上面的例子,我们已经可以得出,
useState
的返回值是一个数组,其内部元素依次为当前state
以及更新state
的函数, 每定义一个state
都需要去成对的获取一下修改相应的state
的方法。
useState(initialValue)
其是一个惰性的初始值,一旦初始化之后,后续initialValue
就算有更新也会被忽略
state Hook中的事件处理函数
对于上述例子,更新state
时传入的事件处理函数,注意不能直接写成:
<button onClick={setCounter(counter + 1)}>click to plus counter</button> // error!
而是得写成回调形式,否则会直接执行一次setCounter(counter + 1)
造成无限循环render
:
<button onClick={() => setCounter(counter + 1)}>click to plus counter</button> // correct!
定义多个state
当想要定义多个state
时,重复调用多次useState
就行了:
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: '学习 Hook' }]);
但是在开发时要注意state
的分离颗粒度。
state hook的更新
值得注意的是,
useState
返回的修改state
的方法对于state
的修改,是单纯的替换而不是合并
useState
返回的修改state
的方法对于state
的修改,是单纯的替换而不是合并,来看一个例子:
import React, {useState} from "react";
export default () => {
const [obj, setObj] = useState({
name: "Yang",
age: 23,
gender: "male"
});
let changeObj = () => (
setObj({
name: "Zhang"
})
);
return (
<div>
<h1>LearnHooks</h1>
{
Object.keys(obj).map(key => {
return (
<div key={key}>key: {obj[key]}</div>
)
})
}
<button onClick={changeObj}>click to changeObj</button>
</div>
)
}
上述例子中,调用setObj({name: "Zhang"})
之后,
obj
的值由{name: "Yang", age: 23, gender: "male"}
直接变为了{name: "Zhang"}
,
并没有像传统的class
组件中调用setState
那样对值进行合并,这一点要特别注意。
且State Hook
对于state
的更新方法,也像class
那样可以传入一个函数进行函数式更新:
setCounter((prevCounter) => {
return prevCounter + 1;
})
Effect Hook
Effect Hook
是针对于那些副作用操作(比如:数据获取,设置订阅以及手动更改React
组件中的DOM
等)而使用的。
和class
组件做比较的话,Effect Hook
可以视为componentDidMount
,componentDidUpdate
和componentWillUnmount
这三个钩子的组合。
无需清除的effect的简单使用
来看一个简单的例子:
import React, {useState, useEffect} from "react";
export default () => {
const [counter, setCounter] = useState(0);
useEffect(() => {
// 第一次渲染之后和每次更新之后都会执行
document.title = `current Counter: ${counter}`
});
return (
<div>
<h1>LearnHooks</h1>
<div>current Counter: {counter}</div>
<button onClick={() => setCounter(counter + 1)}>click to plus counter</button>
</div>
)
}
仔细的来分析一下这个最简单的例子:
首先我们定义了一个无需清除的useEffect
,其内部接收一个函数作为参数,其内部的逻辑在默认情况下,在组件第一次渲染之后和每次更新之后都会执行。
然后由于其定义在函数内部,所以当前函数组件的state
和props
我们都可以在useEffect
内部访问到
useEffect
传递的函数作为参数,会被称为effect
被React
保存起来,在这个函数内部,可以执行任意的副作用操作,且React
保证了每次运行effect
的同时,DOM
都已经更新完毕。
注意:由于
useEffect
其是异步的,所以不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快,但是如果需要effect
同步执行,请使用useLayoutEffect
上面这样使用useEffect
有一个潜在的好处是,开发者没必要去关心当前这个组件到底是第一次渲染还是处于更新状态
使用Class
组件时经常会有componentDidMount
和componentDidUpdate
中存在相同逻辑的地方,useEffect
使得这部分逻辑获得了简化
需要清除的effect
一般使用Class
组件时,我们会在componentDidMount
进行一些数据的订阅,在componentWillUnmount
中取消这部分的订阅
对于这样的effect
,我们使用useEffect
时就要有不一样的逻辑了
一般使用Class
组件,我们需要将订阅和取消订阅操作放到2个不同的钩子函数中,但是使用useEffect
时,这样的操作是放到一起的。
只要在useEffect
中return
出一个函数后,返回的这个函数就会在执行清除操作时(React
会在组件卸载的时候执行清除操作)调用它,这是useEffect
的一个可选的清除机制
所以一般需要清除的effect
的代码大概像这样:
useEffect(() => {
// 第一次渲染之后和每次更新之后都会执行
Api.subscribeXXX(xxx);
return () => { // return的函数会在React执行清除操作时调用
Api.unsubscribeXXX(xxx);
}
});
接下来看一些effect
常见的进阶用法
使用多个Effect
将不相关的逻辑分离开
使用Class
组件的一个不好的地方就是开发者会被迫将不相关的逻辑放到同一个钩子函数中,跟Vue
的options Api
以及composition Api
是一个道理。
而useEffect
也像useState
一样允许开发者定义多个,可以在同一个useEffect
中专注于同一逻辑。例如:
export default () => {
const [counter, setCounter] = useState(0);
useEffect(() => { // 这个effect 只处理counter相关逻辑
document.title = `current Counter: ${counter}`;
});
useEffect(() => { // 这个effect只处理订阅逻辑
Api.subscribeXXX(xxx);
return () => { // return的函数会在React执行清除操作时调用
Api.unsubscribeXXX(xxx);
}
});
return (
<div>
<h1>LearnHooks</h1>
<div>current Counter: {counter}</div>
<button onClick={() => setCounter(counter + 1)}>click to plus counter</button>
</div>
)
}
如上例所示,我们可以根据代码的用途去定义多个effect
由于effect在每次重渲染时都会执行导致的性能问题及解决方案
我们一直在强调,effect
在组件第一次渲染及之后每次更新都会执行
这样做的好处是解决了Class
组件中经常存在的忘记在componentDidUpdate
钩子中添加组件更新后的逻辑的问题
但是这样每次渲染后都执行清理或者执行effect
也带来了性能问题
传统的Class
组件可以在componentDidUpdate
中进行对比prevProps
或prevState
来进行跳过执行逻辑
相应的,使用useEffect
也有对应的功能:
useEffect(() => { // 这个effect 只处理counter相关逻辑
document.title = `current Counter: ${counter}`;
}, [counter]); // 仅在 counter 更改时更新
我们可以通过给useEffect
传递一个数组作为其第二个参数来达到效果,如果某些特定值在两次重渲染之间没有发生变化,就可以跳过对effect
的调用
值得注意的是,如果数组中有多个元素,即使只有一个元素发生变化,React
也会执行effect
如果想执行只运行一次的effect
(仅在组件挂载和卸载时执行),可以传递一个空数组([]
)作为第二个参数。这就告诉React
你的effect
不依赖于props
或state
中的任何值,所以它永远都不需要重复执行。
如果你传入了一个空数组([]
),effect
内部的props
和state
就会一直会是其初始值。
另外
React
会等待浏览器完成画面渲染之后才会延迟调用useEffect
,因此会使得额外操作很方便。
自定义Hook
自定义Hook
是一个函数,其名称必须以use
开头,函数内部可以调用其他的Hook
。
每次使用自定义Hook
时,其中的所有state
和副作用都是完全隔离独立的。
来看一个使用自定义Hook
的例子
假设现在有一个记录页面已经打开了多少秒的组件如下:
export default () => {
const [seconds, setSeconds] = useState(0);
let timer;
useEffect(() => {
timer = setInterval(() => {
setSeconds(seconds + 1);
}, 1000);
return () => {
console.log("Total Seconds: ", seconds);
clearInterval(timer);
}
});
return (
<div>
<span>页面已经渲染了{seconds}秒</span>
</div>
)
}
这时别的组件刚好也需要这个计时功能,就可以将其内部计数的逻辑单独抽出来,定义为一个自定Hook
,比如我们定义为useSeconds
,内部逻辑为:
// useSeconds.js
import {useState, useEffect} from "react";
export default function useSeconds() {
const [seconds, setSeconds] = useState(0);
let timer;
useEffect(() => {
timer = setInterval(() => {
setSeconds(seconds + 1);
},1000);
return () => {
console.log("Total Seconds: ", seconds);
clearInterval(timer);
}
});
return seconds;
}
此时我们就可以进行使用这个自定义Hook
了,在原来的组件里:
import React from "react";
import useSeconds from "./useSeconds";
export default () => {
const seconds = useSeconds();
return (
<div>
<span>页面已经渲染了{seconds}秒</span>
</div>
)
}
在另外想复用的组件里也可以直接引入使用,且多个自定义Hook
之间的state
和effect
是相互独立的。
当然由于自定义Hook
就是一个函数,也可以通过调用使用传入参数传递信息。
从上例中我们可以看出:自定义Hook
解决了以前在React
组件中无法灵活共享逻辑的问题。
useContext
接收一个context
对象(React.createContext
的返回值)并返回该context
的当前值。该Hook
能够读取context
的值以及订阅context
的变化。
来看一个简单使用的例子, 假设有如下Theme
文件:
import React from "react";
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};
const ThemeContext = React.createContext(themes.light);
export {
themes,
ThemeContext,
}
此时在<App />
中应用这个Context
:
import React, {useState, useEffect} from 'react';
import LearnHooks from "./components/LearnHooks";
import { themes, ThemeContext } from "./components/Theme";
function App() {
const [theme, setTheme] = useState(themes.dark);
useEffect(() => {
setTimeout(() => {
setTheme(themes.light); // 3s后将主题改为白色
}, 3000)
});
return (
<div id="app">
<ThemeContext.Provider value={theme}>
<LearnHooks/>
</ThemeContext.Provider>
</div>
);
}
export default App;
此时在我们的目标组件中,就可以进行使用useContext
来进行获取Context
了:
import React, {useContext} from "react";
import { ThemeContext } from "./Theme";
export default () => {
const theme = useContext(ThemeContext); // 获取theme
return (
<p style={{ background: theme.background, color: theme.foreground }}>
normal Text
</p>
)
}
useReducer
用法:
const [state, dispatch] = useReducer(reducer, initialArg, init);
在 state
逻辑较复杂且包含多个子值,或者下一个state
依赖于之前的state
等场景下,可以用来代替useState()
,同时useReducer
的优势在于还会对深层次组件更新做优化。
useReducer
最多可以接收三个参数:
- 第一个参数
reducer
是(state, action) => newState
类型的函数 - 第二个参数如果在第三个参数未传的情况下,是直接作为
state
的初始值的,但是如果传入了第三个参数,那么初始值为init(initialArg)
- 第三个参数是可选的一个函数,参数为
initialArg
, 返回state
的初始值(传入init
时为惰性的初始化state
)。
看一个基本使用的例子:
import React, {useReducer} from "react";
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function init(initialCount) {
return {count: initialCount};
}
export default () => {
const [state, dispatch] = useReducer(reducer, 0, init);
return (
<div>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</div>
)
}
useCallback
和 useMemo
这2个hook
都是作为性能优化手段来使用的,也能使用其特性达成一些特殊用途。且useMemo
可以实现useCallback
相关用法:
// useCallback:
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
// useMemo:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
useCallback(fn, deps)
相当于useMemo(() => fn, deps)
。
2者都是返回一个memoized
过后的函数/值,第二个参数和useEffect
类似为依赖项,如果依赖项有改变的话,memoized
的值或者函数才会得到更新。
如果依赖传入一个[]
或者依赖未发生改变的话,其memoized
的函数或者值的引用地址是不会改变的(同一块内存区域),利用这一特性,可以配合类似于shouldComponentUpdate
的机制来进行避免重复渲染。
使用useCallback
和useMemo
的场景举例
了解了基本概念之后,这篇文章举了个例子展示了useCallback
及useMemo
的使用。
大概例子是有这么一个防抖函数,在鼠标滑动的时候去触发:
// generateDebounce.js
function generateDebounce(func, delay=1000) {
let timer;
function debounce(...args) {
debounce.cancel();
timer = setTimeout(() => {
console.count("func called");
func.apply(this, args);
}, delay);
}
debounce.cancel = function () {
if (timer !== undefined) {
clearTimeout(timer);
timer = undefined;
}
};
return debounce;
}
这个函数调用之后返回一个防抖函数debounce
,然后在如下组件中进行防抖使用:
import React, {useState} from "react";
import generateDebounce from "./generateDebounce";
export default () => {
const [count, setCount] = useState(0);
const [bounceCount, setBounceCount] = useState(0);
const debounceSetCount = generateDebounce(setBounceCount); // 每次更新渲染都会重新创建一个debounceSetCount
const handleMouseMove = () => {
setCount(count + 1);
debounceSetCount(bounceCount + 1);
};
return (
<div onMouseMove={handleMouseMove}>
<p>普通移动次数: {count}</p>
<p>防抖处理后移动次数: {bounceCount}</p>
</div>
)
}
在上述例子中,我们可以看到,虽然bounceCount
增加的不多,但是其实内部的console.count("func called");
执行的次数和未做防抖的次数count
是一样的
也就是说并没有达到防抖的效果,造成这个现象的原因是:
每次执行onMouseMove
都会导致组件的重新渲染,整个函数组件将会被重新执行
即意味着const debounceSetCount = generateDebounce(setBounceCount);
这句每次都会执行,会创建很多个新的debounceSetCount
,所以其不同的debounce
其实是使用很多个不同的timer
,这就造成了我们看到的调用次数并没有减少的情况
但是bounceCount
增加的并没有像count
那么快的原因就是在执行onMouseMove
时疯狂的传入了很多次一样的参数,而在异步函数中执行增加操作时,其实都是一个相同的值在加一,所以bounceCount
没有增加到函数调用次数那么大,但是本质上,函数还是调用了很多次的。
使用useCallback
举例
花了这么多篇幅讲通这个例子的原路,现在来看怎么修复,我们通过useCallback
创建一个memoized
函数,依赖为[]
, 这样一来,我们创建的这个debounceSetCount
函数的引用就一直是同一个地址,这样就组件每次更新时,由于依赖为[]
,函数一直不会更新,永远为同一个函数,即可达到效果
export default () => {
const [count, setCount] = useState(0);
const [bounceCount, setBounceCount] = useState(0);
// const debounceSetCount = generateDebounce(setBounceCount);
// 改用callback创建一个 memoized 函数,依赖为[]即永远保存同一块内存中的这个 debounceSetCount 函数
const debounceSetCount = useCallback(generateDebounce(setBounceCount), []);
// 省略下面代码。。。。
}
使用useMemo
举例
上面例子中,也可以直接使用useMemo
:
// const debounceSetCount = generateDebounce(setBounceCount);
// const debounceSetCount = useCallback(generateDebounce(setBounceCount), []);
const debounceSetCount = useMemo(() => generateDebounce(setBounceCount), []);
达到的效果是一样的。也能创建一个唯一的debounceSetCount
函数
关于useMemo
,官方建议我们,先不要使用useMemo
编写可用的代码,然后再引入useMemo
仅仅作为性能优化的手段,因为官方说了useMemo
不一定能作为一个保证来使用
关于
useMemo
引自文档: You may rely on useMemo as a performance optimization, not as a semantic guarantee. In the future, React may choose to “forget” some previously memoized values and recalculate them on next render, e.g. to free memory for offscreen components.
useRef
用法:
const ref = useRef(initialValue);
和Class
组件一样,useRef
提供了在函数组件中使用ref
的方法,其参数initialValue
为给ref
设置的初始值,该值在useEffect
之中就已经被重新赋值为目标DOM
来看使用例子:
import React, {useRef, useEffect} from "react";
export default () => {
const testRef = useRef(null); // 给null作为初始值
console.log(testRef); // {current: null}
useEffect(() => {
console.log(testRef); // 输出 {current: div}
});
return (
<div ref={testRef}>
normal Text
</div>
)
}
当
ref
对象内容发生变化时,useRef
并不会通知更新。且变更.current
属性也不会引发组件重新渲染。
如果想要在React
绑定或解绑DOM
节点的ref
时运行某些代码,则需要使用回调ref
来实现。
来看一个不使用useRef
而是使用回调ref
的例子:
export default () => {
const [isShow, setIsShow] = useState(true);
const callbackRef = useCallback((domNode) => {
console.log(domNode); // 在ref附加到节点上时自动调用 在节点卸载时也会自动调用 输出null
}, []);
return (
<React.Fragment>
{
isShow &&
<h1 ref={callbackRef}>
<div>Hello, ref</div>
</h1>
}
<button onClick={() => (setIsShow(false))}>click</button>
</React.Fragment>
)
}
我们分析下上述例子:使用useCallback
声明一个callbackRef
,传入的依赖为[]
,所以其ref
不会在组件重新渲染时改变。
使用回调ref
的优点是,节点发生变化的时候,会自动调用目标回调,而使用useRef
时,节点对象发生变化时,useRef
并不会通知你(当然可以手动写一个useEffect
去主动获取ref
对象,是可以拿到最新的对象的)
useImperativeHandle
用法:
useImperativeHandle(ref, createHandle, [deps])
useImperativeHandle
是和forwardRef
搭配使用实现refs
转发的,来看使用例子,现有父组件:
export default () => {
const supRef = useRef(null);
useEffect(() => {
console.log(supRef);
supRef.current.focus();
});
return (
<ImperativeHandle ref={supRef} />
)
}
而子组件里的逻辑为:
// ImperativeHandle.js
import React, {useRef, useImperativeHandle} from "react";
export default React.forwardRef((props, ref) => {
const subRef = useRef(null);
useImperativeHandle(ref, () => subRef.current);
return <input ref={subRef} />;
})
而useImperativeHandle
的功能在于,在使用ref
时自定义暴露给父组件的实例值,上述例子中我们通过使用:
useImperativeHandle(ref, () => subRef.current);
直接暴露出了整个subRef.current
,我们可以自定义决定暴露出什么,比如我们改为暴露一个subFocus
方法而不是暴露整个subRef
:
// ImperativeHandle.js
<!--省略其他代码-->
useImperativeHandle(ref, () => { // 可以自定义决定暴露什么内容给父组件
return {
subFocus: () => {
subRef.current.focus();
}
}
});
在父组件中获取到的supRef.current
也发生了相应的改变:
export default () => {
const supRef = useRef(null);
useEffect(() => {
// 在这获取到的supRef.current 就是子组件通过 useImperativeHandle 自定义暴露出的内容
supRef.current.subFocus(); // 调用暴露出的subFocus()
});
return (
<ImperativeHandle ref={supRef} />
)
}
useLayoutEffect
useLayoutEffect
和useEffect
的区别在于:useLayoutEffect
会在所有的DOM
变更之后同步调用effect
。
可以使用它来读取DOM
布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect
内部的更新计划将被同步刷新。
其调用阶段和componentDidMount
、componentDidUpdate
的调用阶段是一样的
但是一般只有特殊情况才会使用到,一般建议使用useEffect
来避免阻塞加载从而提高用户体验
一些补充
react hooks 异步获取数据
扩展阅读:How to fetch data with React Hooks?
useRef
的额外用法
useRef
不仅可以用于DOM refs
。ref
对象还是一个current
属性可变且可以容纳任意值的通用容器,可以作为当前组件中的全局变量进行使用。
比如说:作为一个timerID
,在卸载组件的时候进行消除
使用useRef
让effect
只在组件更新时执行
通过使用useRef
,可以达到一个使useEffect
只在组件更新时(类似于componentDidUpdate
)进行执行effect
而在组件第一次渲染时(类似于componentDidMount
)不执行effect
:
export default () => {
const [counter, setCounter] = useState(0);
const isFirstRender = useRef(true); // 设置默认 是否第一次渲染为true
useEffect(() => {
if(!isFirstRender.current) { // 已经不是第一次渲染 而是后续组件更新
console.log("componentDidUpdate");
// 目标 effect的逻辑可以在这执行
}else {
isFirstRender.current = false; // 第一次渲染之后将值置为false
console.log("componentDidMount");
}
});
return (
<div>
<div>counter: {counter}</div>
<button onClick={() => setCounter(counter + 1)}>click to reRender component</button>
</div>
)
}
通过useRef
获取上一轮的props
或者state
可以通过useRef
和useEffect
来进行记录存储上一次的state
:
export default () => {
const [counter, setCounter] = useState(0);
const prevCounter = useRef();
useEffect(() => {
prevCounter.current = counter;
console.log("counter: ", counter);
console.log("prevCounter: ", prevCounter.current); // 这里获取的prevCounter和counter是一致的
// 这的逻辑是较晚异步执行的
});
console.log(prevCounter.current); // 在这里获取的prevCounter 为前一次的值
// useEffect是异步执行 所以在这的逻辑是较早执行的
return (
<div>
<div>counter: {counter}</div>
<div>prevCounter: {prevCounter.current}</div>
<button onClick={() => setCounter(counter + 1)}>click to reRender component</button>
</div>
)
}
如果该逻辑经常用到的话,可以考虑封装为一个自定义hook
:
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
组件中的函数读取state
和prop
出现不及时更新的情况
造成在组件内函数中拿到的state
和prop
不是最新的原因有两个:
- 如果是使用的
useEffect
,可能是依赖数组中提供了[]
或者依赖项没有提供全. - 组件内部的任何函数,包括事件处理函数和
effect
,其内部拿到的值都是其被声明的那一次渲染中获取的
其中第1点可能很好理解,解决方案就是修正给useEffect
提供的依赖数组即可
下面解释一下第2点:假设现有如下例子:
export default () => {
const [counter, setCounter] = useState(0);
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + counter);
}, 3000);
}
return (
<div>
<p>You clicked {counter} times</p>
<button onClick={() => setCounter(counter + 1)}>
Click me
</button>
<button onClick={handleAlertClick}>
Show alert
</button>
</div>
)
}
在上述组件中,点击了show alert
按钮之后,再点击数次click me
按钮去增加counter
可以看到,3s后会输出当时3s前点击show alert
时的counter
,而不是目前页面显示的counter
要理解这种情况发生的原因,需要理解2点:
- 每次点击
click me
去更新state
的时候,整个函数组件的逻辑都会被重新执行,所以事件处理函数handleAlertClick
每次都会被重复声明 - 明确了第1点之后,那么每次重新声明的
handleAlertClick
内部都只能拿到当前这次渲染中的state
我们在上例中,先点击一次handleAlertClick
,其只能拿到当前这次渲染时的counter
即0
,然后我们点击数次click me
并不会改变第一次声明的这个handleAlertClick
中拿到的counter
值,所以即会造成上述情况。
而这种情况,也是会造成在函数中拿到的值是陈旧的情况,针对这种情况,如果想要去获取到最新的state
和prop
的话,可以在值更新后的异步回调中去创建一个ref
去存储其最新值。
使用ref
存储的例子:
export default () => {
const [counter, setCounter] = useState(0);
const latestVal = useRef(null);
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + counter);
alert('latest counter: ' + latestVal.current);
}, 3000);
}
useEffect(() => {
latestVal.current = counter; // 在这使用ref存储最新的值
}, [counter]);
return (
<div>
<p>You clicked {counter} times</p>
<button onClick={() => setCounter(counter + 1)}>
Click me
</button>
<button onClick={handleAlertClick}>
Show alert
</button>
</div>
)
}
在函数组件中使用hook
达到static getDerivedStateFromProps
的效果
如果你的组件中
state
的值在任何时候都取决于props
的时候,这种情况才考虑使用static getDerivedStateFromProps
, 用之前考虑一下,如果不是这种情况,那么you-probably-dont-need-derived-state
针对这种情况,React
有一个机制是:如果在渲染过程中更新state
的话,那么React
会立即退出上一次渲染并用更新后的 state
重新运行组件以避免耗费太多性能
function ScrollView({row}) {
let [isScrollingDown, setIsScrollingDown] = useState(false);
let [prevRow, setPrevRow] = useState(null);
// 每次父组件props.row改变时,在这做拦截判断,如果没改变那么按逻辑return,
// 如果props.row改变了,那么直接在这setState更新,跳过当前这次渲染,直接使用新的state运行下次组件逻辑
if (row !== prevRow) {
// Row 自上次渲染以来发生过改变。更新 isScrollingDown。
setIsScrollingDown(prevRow !== null && row > prevRow);
setPrevRow(row);
}
return `Scrolling down: ${isScrollingDown}`;
}
上述是官方举的一个例子,该组件接收一个props
为row
,目标组件中state
依赖props.row
进行更新,本组件中如果父组件滚动时,子组件的逻辑会判断props.row
,如果确定向下滚动了,那么直接调用setState
更新state
,跳过当前这次渲染,直接使用新的state
运行下次组件逻辑
实现类似于React.PureComponent
的效果
可以通过React.memo
来达到效果:
const Button = React.memo((props) => {
// 你的组件
});
React.memo
等效于PureComponent
,但它只比较props
, 不比较state
。
也可以通过第二个参数指定一个自定义的比较函数来比较新旧 props。如果函数返回 true,就会跳过更新
// 第二个参数指定一个自定义的比较函数来比较新旧 props
const compare = (prevProp, currentProp) => {
return prevProp.children === currentProp.children; // return true 代表跳过更新
};
export default React.memo((props) => {
return (
<div>
<div>hello world</div>
{props.children}
</div>
)
}, compare)